package com.dbflow5.processor.definition.column;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.*;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.BaseDefinition;
import com.dbflow5.processor.definition.EntityDefinition;
import com.dbflow5.processor.definition.TableDefinition;
import com.dbflow5.processor.definition.behavior.Behaviors;
import com.dbflow5.processor.definition.behavior.ComplexColumnBehavior;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

public class ColumnDefinition extends BaseDefinition {
    public EntityDefinition entityDefinition;
    public Column column;

    public ColumnDefinition(ProcessorManager processorManager,
                            Element element,
                            EntityDefinition entityDefinition,
                            boolean isPackagePrivate,
                            Column column,
                            PrimaryKey primaryKey,
                            ConflictAction notNullConflict) {
        super(element, processorManager, null);
        init(processorManager, element, isPackagePrivate, primaryKey, notNullConflict);
        this.entityDefinition = entityDefinition;
        this.column = column;
    }

    public static class Type {
        private Type() {
        }

        public static final class Normal extends ColumnDefinition.Type{
            public static final ColumnDefinition.Type.Normal INSTANCE;

            static {
                INSTANCE = new Normal();
            }

            private Normal() {
                super();
            }
        }

        public static final class Primary extends ColumnDefinition.Type{
            public static final ColumnDefinition.Type.Primary INSTANCE;

            static {
                INSTANCE = new Primary();
            }

            private Primary() {
                super();
            }
        }

        public static final class RowId extends ColumnDefinition.Type{
            public static final ColumnDefinition.Type.RowId INSTANCE;

            static {
                INSTANCE = new RowId();
            }

            private RowId() {
                super();
            }
        }

        public static final class PrimaryAutoIncrement extends ColumnDefinition.Type{
            public boolean quickCheck;

            private PrimaryAutoIncrement(boolean quickCheck) {
                super();
                this.quickCheck = quickCheck;
            }
        }

        public final boolean isPrimaryField() {
            return this instanceof Primary
                || this instanceof PrimaryAutoIncrement
                || this instanceof ColumnDefinition.Type.RowId;
        }
    }

    private final Pattern QUOTE_PATTERN = Pattern.compile("\".*\"");

    public String columnName = "";
    public String propertyFieldName = "";

    public Type type = Type.Normal.INSTANCE;

    //var isQuickCheckPrimaryKeyAutoIncrement: Boolean = false
    public int length = -1;
    public boolean notNull = false;
    public boolean isNotNullType = false;
    public boolean isNullableType = true;
    public ConflictAction onNullConflict = null;
    public ConflictAction onUniqueConflict = null;
    public boolean unique = false;

    public List<Integer> uniqueGroups = new ArrayList<>();
    public List<Integer> indexGroups = new ArrayList<>();

    public Collate collate = Collate.NONE;
    public String defaultValue = null;

    public ColumnAccessor columnAccessor;

    public ComplexColumnBehavior complexColumnBehavior;

    public ColumnAccessCombiner.Combiner combiner;

    public CodeBlock updateStatementBlock() {
        return CodeBlock.of(StringUtils.quote(columnName) + "=?");
    }

    public CodeBlock insertStatementColumnName() {
        return CodeBlock.of("$L", StringUtils.quote(columnName));
    }

    public CodeBlock insertStatementValuesString() {
        if (type instanceof Type.PrimaryAutoIncrement && isNotNullType) {
            return CodeBlock.of("nullif(?, 0)");
        } else {
            return CodeBlock.of("?");
        }
    }

    public List<TypeName> typeConverterElementNames() {
        return Collections.singletonList(elementTypeName);
    }

    public String primaryKeyName() {
        return StringUtils.quote(columnName);
    }

    private void init(ProcessorManager processorManager,
                      Element element,
                      boolean isPackagePrivate,
                      PrimaryKey primaryKey,
                      ConflictAction notNullConflict) {
        NotNull notNullAnno = element.getAnnotation(NotNull.class);
        if(notNullAnno != null) {
            notNull = true;
            onNullConflict = notNullAnno.onNullConflict();
        }

        if (onNullConflict == ConflictAction.NONE && notNullConflict != ConflictAction.NONE) {
            onNullConflict = notNullConflict;
            notNull = true;
        }

        if (elementTypeName != null && elementTypeName.isPrimitive()) {
            isNullableType = false;
            isNotNullType = true;
        }

        // if specified, usually from Kotlin targets, we will not set null on the field.
        org.jetbrains.annotations.NotNull notNull = element.getAnnotation(org.jetbrains.annotations.NotNull.class);
        if(notNull != null) {
            isNotNullType = true;
            isNullableType = false;
        }

        // ohos support annotation
        List<? extends AnnotationMirror> list = element.getAnnotationMirrors();
        for(AnnotationMirror it : list) {
            ClassName className = ElementExtensions.toClassName(ElementExtensions.toTypeElement(it.getAnnotationType(), ProcessorManager.manager), ProcessorManager.manager);
            if(className == ClassNames.NON_NULL || className == ClassNames.NON_NULL_X) {
                isNotNullType = true;
                isNullableType = false;
                break;
            }
        }

        if(column != null){
            if("".equals(column.name())) {
                this.columnName = element.getSimpleName().toString();
            }else {
                this.columnName = column.name();
            }
            length = column.length;
            collate = column.collate();
            defaultValue = column.defaultValue();

            if (StringUtils.isNullOrEmpty(column.defaultValue())) {
                defaultValue = null;
            }
        }

        if (column == null) {
            this.columnName = element.getSimpleName().toString();
        }

        boolean isString = (elementTypeName == ClassName.get(String.class));
        if (defaultValue != null
            && isString
            && !QUOTE_PATTERN.matcher(defaultValue).find()) {
            defaultValue = "\"$defaultValue\"";
        }

        if (isNotNullType && defaultValue == null
            && isString) {
            defaultValue = "\"\"";
        }

        NameAllocator nameAllocator = new NameAllocator();
        propertyFieldName = nameAllocator.newName(this.columnName);

        if (isPackagePrivate) {
            columnAccessor = new ColumnAccessor.PackagePrivateScopeColumnAccessor(elementName, packageName,
                ClassName.get((TypeElement)element.getEnclosingElement()).simpleName());

            ColumnAccessor.PackagePrivateScopeColumnAccessor.putElement(
                ((ColumnAccessor.PackagePrivateScopeColumnAccessor)columnAccessor).helperClassName, columnName);

        } else {
            boolean isPrivate = element.getModifiers().contains(Modifier.PRIVATE);
            if (isPrivate) {
                boolean isBoolean = elementTypeName != null && elementTypeName.box() == TypeName.BOOLEAN.box();
                boolean useIs = isBoolean
                    && entityDefinition instanceof TableDefinition && ((TableDefinition) entityDefinition).useIsForPrivateBooleans;
                columnAccessor = new ColumnAccessor.PrivateScopeColumnAccessor(elementName, new ColumnAccessor.GetterSetter() {
                    @Override
                    public String getterName() {
                        return column != null ?column.getterName() : "";
                    }

                    @Override
                    public String setterName() {
                        return column != null ?column.setterName() : "";
                    }
                }, useIs, "");
            } else {
                columnAccessor = new ColumnAccessor.VisibleScopeColumnAccessor(elementName);
            }
        }

        if (primaryKey != null) {
            if(primaryKey.rowID()) {
                type = Type.RowId.INSTANCE;
            }else if(primaryKey.autoincrement()) {
                type = new Type.PrimaryAutoIncrement(primaryKey.quickCheckAutoIncrement());
            }else {
                type = Type.Primary.INSTANCE;
            }
        }

        Unique uniqueColumn = element.getAnnotation(Unique.class);
        if(uniqueColumn != null){
            unique = uniqueColumn.unique();
            onUniqueConflict = uniqueColumn.onUniqueConflict();
            for(int it : uniqueColumn.uniqueGroups()) {
                uniqueGroups.add(it);
            }
        }

        Index index = element.getAnnotation(Index.class);
        if(index != null) {
            // empty index, we assume generic
            if (index.indexGroups().length == 0) {
                indexGroups.add(IndexGroup.INDEX_GENERIC);
            } else {
                for(int it : index.indexGroups()) {
                    indexGroups.add(it);
                }
            }
        }

        TypeMirror typeMirror = null;
        if(column != null){
            typeMirror = ProcessorUtils.extractTypeMirrorFromAnnotation(column, e -> {
                column.typeConverter();
                return null;
            }, column1 -> null);
        }

        ClassName typeConverterClassName = null;
        if(typeMirror != null){
            typeConverterClassName = ProcessorUtils.fromTypeMirror(typeMirror, manager);
        }

        complexColumnBehavior = new ComplexColumnBehavior(
            elementTypeName,
            this,
            this,
            false,
            typeConverterClassName,
            typeMirror,
            manager
        );

        combiner = new ColumnAccessCombiner.Combiner(columnAccessor, elementTypeName, complexColumnBehavior.wrapperAccessor,
            complexColumnBehavior.wrapperTypeName,
            complexColumnBehavior.subWrapperAccessor, "");
    }

    @Override
    public String toString() {
        EntityDefinition tableDef = entityDefinition;
        String tableName = tableDef.elementName;
        if (tableDef instanceof TableDefinition) {
            tableName = tableDef.associationalBehavior().name;
        }
        return entityDefinition.databaseDefinition().elementName+"."+tableName+"."+StringUtils.quote(columnName);
    }

    public void addPropertyDefinition(TypeSpec.Builder typeBuilder, TypeName tableClass) {
        if(elementTypeName != null){
            boolean isNonPrimitiveTypeConverter = !complexColumnBehavior.wrapperAccessor.isPrimitiveTarget()
                    && complexColumnBehavior.wrapperAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor;
            TypeName propParam;
            if (isNonPrimitiveTypeConverter) {
                propParam = ParameterizedTypeName.get(ClassNames.TYPE_CONVERTED_PROPERTY, complexColumnBehavior.wrapperTypeName, elementTypeName.box());
            } else if (!complexColumnBehavior.wrapperAccessor.isPrimitiveTarget()) {
                propParam = ParameterizedTypeName.get(ClassNames.WRAPPER_PROPERTY, complexColumnBehavior.wrapperTypeName, elementTypeName.box());
            } else {
                propParam = ParameterizedTypeName.get(ClassNames.PROPERTY, elementTypeName.box());
            }

            FieldSpec.Builder fieldBuilder = FieldSpec.builder(propParam,
                    propertyFieldName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);

            if (isNonPrimitiveTypeConverter) {
                CodeBlock.Builder codeBlock = CodeBlock.builder();
                codeBlock.add("new $T($T.class, $S, true,", propParam, tableClass, columnName);
                codeBlock.add("\"" +
                    "new \"$\"T() {" +
                    "@Override" +
                    "public \"$\"T getTypeConverter(Class<?> modelClass) {" +
                        "\"$\"T adapter = (\"$\"T) \"$\"T.getRetrievalAdapter(modelClass);" +
                        "return adapter.\"$\"L;" +
                    "}" +
                    "})"+"\"",
                        ClassNames.TYPE_CONVERTER_GETTER, ClassNames.TYPE_CONVERTER,
                        entityDefinition.outputClassName, entityDefinition.outputClassName,
                        ClassNames.FLOW_MANAGER,
                        ((ColumnAccessor.TypeConverterScopeColumnAccessor)complexColumnBehavior.wrapperAccessor).typeConverterFieldName);
                fieldBuilder.initializer(codeBlock.build());
            } else {
                fieldBuilder.initializer("new $T($T.class, $S)", propParam, tableClass, columnName);
            }
            if (type instanceof Type.Primary) {
                fieldBuilder.addJavadoc("Primary Key");
            } else if (type instanceof Type.PrimaryAutoIncrement) {
                fieldBuilder.addJavadoc("Primary Key AutoIncrement");
            }
            typeBuilder.addField(fieldBuilder.build());
        }
    }

    public void addPropertyCase(MethodSpec.Builder methodBuilder) {
        methodBuilder.beginControlFlow("case $S: ", StringUtils.quote(columnName));
        methodBuilder.addStatement("return $L", propertyFieldName);
        methodBuilder.endControlFlow();
    }

    public void addColumnName(CodeBlock.Builder codeBuilder) {
        codeBuilder.add(propertyFieldName);
    }

    public CodeBlock contentValuesStatement() {
        CodeBlock.Builder code = CodeBlock.builder();

        ColumnAccessCombiner.ContentValuesCombiner contentValuesCombiner = new ColumnAccessCombiner.ContentValuesCombiner(combiner);
        contentValuesCombiner.addCode(code, columnName, getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
        return code.build();
    }

    public void appendIndexInitializer(CodeBlock.Builder initializer, AtomicInteger index) {
        if (index.get() > 0) {
            initializer.add(", ");
        }
        initializer.add(columnName);
        index.incrementAndGet();
    }

    public CodeBlock getSQLiteStatementMethod(AtomicInteger index, boolean defineProperty) {
        CodeBlock.Builder builder = CodeBlock.builder();

        ColumnAccessCombiner.SqliteStatementAccessCombiner sqliteStatementAccessCombiner =  new ColumnAccessCombiner.SqliteStatementAccessCombiner(combiner);
        sqliteStatementAccessCombiner.addCode(builder,"", getDefaultValueBlock(), index.get(), ColumnAccessor.modelBlock, defineProperty);
        return builder.build();
    }

    public CodeBlock getLoadFromCursorMethod(boolean endNonPrimitiveIf, AtomicInteger index, NameAllocator nameAllocator) {
        CodeBlock.Builder builder = CodeBlock.builder();

        Behaviors.CursorHandlingBehavior behavior = entityDefinition.cursorHandlingBehavior();
        boolean orderedCursorLookup = behavior.orderedCursorLookup;
        boolean assignDefaultValuesFromCursor = behavior.assignDefaultValuesFromCursor;
        boolean assignDefaultValue = assignDefaultValuesFromCursor;
        CodeBlock defaultValueBlock = getDefaultValueBlock();
        if (isNotNullType && CodeBlock.of("null") == defaultValueBlock) {
            assignDefaultValue = false;
        }

        ColumnAccessCombiner.LoadFromCursorAccessCombiner loadFromCursorAccessCombiner = new ColumnAccessCombiner.LoadFromCursorAccessCombiner(combiner, defaultValue != null,
                nameAllocator,
                new Behaviors.CursorHandlingBehavior(orderedCursorLookup, assignDefaultValue));
        loadFromCursorAccessCombiner.addCode(builder, columnName, getDefaultValueBlock(), index.get(), ColumnAccessor.modelBlock, false);
        return builder.build();
    }

    /**
     * only used if [.isPrimaryKeyAutoIncrement] is true.

     * @return The statement to use.
     */
    public CodeBlock updateAutoIncrementMethod() {
        CodeBlock.Builder builder = CodeBlock.builder();
        ColumnAccessCombiner.UpdateAutoIncrementAccessCombiner updateAutoIncrementAccessCombiner = new ColumnAccessCombiner.UpdateAutoIncrementAccessCombiner(combiner);
        updateAutoIncrementAccessCombiner.addCode(builder, columnName, getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
        return builder.build();
    }

    public CodeBlock getColumnAccessString(int index) {
        CodeBlock.Builder builder = CodeBlock.builder();
        ColumnAccessCombiner.CachingIdAccessCombiner updateAutoIncrementAccessCombiner = new ColumnAccessCombiner.CachingIdAccessCombiner(combiner);
        updateAutoIncrementAccessCombiner.addCode(builder, columnName, getDefaultValueBlock(), index, ColumnAccessor.modelBlock, false);
        return builder.build();
    }

    public CodeBlock getSimpleAccessString() {
        CodeBlock.Builder builder = CodeBlock.builder();
        ColumnAccessCombiner.SimpleAccessCombiner updateAutoIncrementAccessCombiner = new ColumnAccessCombiner.SimpleAccessCombiner(combiner);
        updateAutoIncrementAccessCombiner.addCode(builder, columnName, getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
        return builder.build();
    }

    public boolean quickCheckPrimaryKey() {
        if (type instanceof Type.PrimaryAutoIncrement){
            return ((Type.PrimaryAutoIncrement) type).quickCheck;
        }

        return false;
    }

    public boolean isAutoRowId() {
        return type instanceof Type.RowId || type instanceof Type.PrimaryAutoIncrement;
    }

    public boolean shouldWriteExistence() {
        return isAutoRowId() || quickCheckPrimaryKey();
    }

    public void appendExistenceMethod(CodeBlock.Builder codeBuilder) {
        ColumnAccessCombiner.ExistenceAccessCombiner existenceAccessCombiner = new ColumnAccessCombiner.ExistenceAccessCombiner(combiner, isAutoRowId(),
            quickCheckPrimaryKey(),
            entityDefinition.elementClassName);
        existenceAccessCombiner.addCode(codeBuilder, columnName, getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
    }

    public void appendPropertyComparisonAccessStatement(CodeBlock.Builder codeBuilder) {
        ColumnAccessCombiner.PrimaryReferenceAccessCombiner primaryReferenceAccessCombiner = new ColumnAccessCombiner.PrimaryReferenceAccessCombiner(combiner);
        primaryReferenceAccessCombiner.addCode(codeBuilder, propertyFieldName, getDefaultValueBlock(), 0, ColumnAccessor.modelBlock, false);
    }

    public CodeBlock creationName() {
        CodeBlock.Builder codeBlockBuilder = DefinitionUtils.getCreationStatement(elementTypeName, complexColumnBehavior.wrapperTypeName, columnName);

        if (type instanceof Type.PrimaryAutoIncrement) {
            codeBlockBuilder.add(" PRIMARY KEY ");

            if (entityDefinition instanceof TableDefinition &&
            !StringUtils.isNullOrEmpty(((TableDefinition) entityDefinition).primaryKeyConflictActionName)) {
                codeBlockBuilder.add("ON CONFLICT $L ", ((TableDefinition) entityDefinition).primaryKeyConflictActionName);
            }

            codeBlockBuilder.add("AUTOINCREMENT");
        }

        if (length > -1) {
            codeBlockBuilder.add("($L)", length);
        }

        if (collate != Collate.NONE) {
            codeBlockBuilder.add(" COLLATE $L", collate);
        }

        if (unique) {
            codeBlockBuilder.add(" UNIQUE ON CONFLICT $L", onUniqueConflict);
        }

        if (notNull) {
            codeBlockBuilder.add(" NOT NULL ON CONFLICT $L", onNullConflict);
        }

        return codeBlockBuilder.build();
    }

    public CodeBlock getDefaultValueBlock(String value, TypeName elementTypeName) {
        String defaultValue = value;
        if (!defaultValue.isEmpty()) {
            defaultValue = "null";
        }
        if (elementTypeName != null && elementTypeName.isPrimitive()) {
            if (elementTypeName == TypeName.BOOLEAN) {
                defaultValue = "false";
            } else if (elementTypeName == TypeName.BYTE || elementTypeName == TypeName.INT
                || elementTypeName == TypeName.DOUBLE || elementTypeName == TypeName.FLOAT
                || elementTypeName == TypeName.LONG || elementTypeName == TypeName.SHORT) {
                defaultValue = "($elementTypeName) 0";
            } else if (elementTypeName == TypeName.CHAR) {
                defaultValue = "'\\u0000'";
            }
        }
        return CodeBlock.of(defaultValue);
    }

    public CodeBlock getDefaultValueBlock() {
        return getDefaultValueBlock(defaultValue, elementTypeName);
    }
}
